/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.beepmystuff;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.createStrictMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;

import com.google.beepmystuff.BmsApiException.ErrorCode;

import junit.framework.TestCase;

public class BmsClientTest extends TestCase {
  private final BmsClient.SessionStorage sessionStore =
      createMock(BmsClient.SessionStorage.class);
  private final BmsLowLevelClient lowLevelClient =
      createStrictMock(BmsLowLevelClient.class);  // order matters!
  private final BmsClient client = new BmsClient(lowLevelClient, sessionStore);

  public void testShouldStoreSessionTokenAfterLogin() throws Exception {
    BmsClient.LoginResult result = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "sessiony";
      }
    };
    expect(lowLevelClient.login("nickname", "password")).andReturn(result);
    sessionStore.setSessionToken("sessiony");
    replay(lowLevelClient);
    replay(sessionStore);

    assertEquals(result, client.login("nickname", "password"));
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldPropagateExceptionsRaisedDuringLogin() throws Exception {
    replay(sessionStore);

    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_USERNAME, "Fail.");
    expect(lowLevelClient.login("nickname", "password")).andThrow(exception);
    replay(lowLevelClient);

    try {
      client.login("nickname", "password");
      fail("Expected BmsApiException");
    } catch(BmsApiException e) {
      assertSame(e, exception);
    }
    verify(lowLevelClient);
  }

  public void testShouldRequestSessionTokenIfNoneAvailable() throws Exception {
    // Set credentials without attempting to login.
    client.setCredentials("nickname", "password");

    // Now we can try something that requires a session token.
    BmsClient.LoginResult loginResult = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "sessiony";
      }
    };
    expect(sessionStore.getSessionToken()).andReturn(null);
    expect(lowLevelClient.login("nickname", "password")).andReturn(loginResult);
    sessionStore.setSessionToken("sessiony");
    expect(lowLevelClient.getAvatarPath("sessiony")).andReturn("somepath");
    replay(lowLevelClient);
    replay(sessionStore);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldReuseLastUsedSessionToken() throws Exception {
    // Repeat the test above: no session token known, none available in storage.

    client.setCredentials("nickname", "password");
    BmsClient.LoginResult loginResult = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "sessiony";
      }
    };
    expect(sessionStore.getSessionToken()).andReturn(null);
    expect(lowLevelClient.login("nickname", "password")).andReturn(loginResult);
    sessionStore.setSessionToken("sessiony");
    expect(lowLevelClient.getAvatarPath("sessiony")).andReturn("somepath");
    replay(lowLevelClient);
    replay(sessionStore);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
    verify(sessionStore);

    // Now, the next request should use the cached session token without
    // consulting the session store.
    reset(lowLevelClient);
    reset(sessionStore);

    replay(sessionStore);
    expect(lowLevelClient.getAvatarPath("sessiony")).andReturn("somepath");
    replay(lowLevelClient);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
  }

  public void testShouldUseStoredSessionToken() throws Exception {
    // Don't set any credentials.

    expect(sessionStore.getSessionToken()).andReturn("sessiony");
    replay(sessionStore);
    expect(lowLevelClient.getAvatarPath("sessiony")).andReturn("somepath");
    replay(lowLevelClient);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
    verify(sessionStore);

    // Again, the next request should use the cached session token without
    // consulting the session store.
    reset(lowLevelClient);
    reset(sessionStore);

    replay(sessionStore);
    expect(lowLevelClient.getAvatarPath("sessiony")).andReturn("somepath");
    replay(lowLevelClient);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
  }

  public void testShouldPropagateNonSessionException() throws Exception {
    // Don't set any credentials.

    expect(sessionStore.getSessionToken()).andReturn("sessiony");
    replay(sessionStore);
    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_KEY, "Fail.");
    expect(lowLevelClient.getAvatarPath("sessiony")).andThrow(exception);
    replay(lowLevelClient);

    try {
      client.getAvatarPath();
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertSame(exception, e);
    }
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldRequestSessionTokenIfExpired() throws Exception {
    client.setCredentials("nickname", "password");

    expect(sessionStore.getSessionToken()).andReturn("sessiony");
    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_SESSION, "Ohno!");
    expect(lowLevelClient.getAvatarPath("sessiony")).andThrow(exception);
    sessionStore.setSessionToken(null);
    BmsClient.LoginResult loginResult = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "newsessiony";
      }
    };
    expect(lowLevelClient.login("nickname", "password")).andReturn(loginResult);
    sessionStore.setSessionToken("newsessiony");
    expect(lowLevelClient.getAvatarPath("newsessiony")).andReturn("somepath");
    replay(sessionStore);
    replay(lowLevelClient);

    assertEquals("somepath", client.getAvatarPath());
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldOnlyRequestSessionTokenOnce() throws Exception {
    // Infinite loops of 'session expired' are bad, so we only retry once.
    client.setCredentials("nickname", "password");

    expect(sessionStore.getSessionToken()).andReturn("sessiony");
    expect(lowLevelClient.getAvatarPath("sessiony")).andThrow(
        new BmsApiException(ErrorCode.INVALID_SESSION, "Ohno!"));
    sessionStore.setSessionToken(null);
    BmsClient.LoginResult loginResult = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "newsessiony";
      }
    };
    expect(lowLevelClient.login("nickname", "password")).andReturn(loginResult);
    sessionStore.setSessionToken("newsessiony");
    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_SESSION, "Still Ohno!");
    expect(lowLevelClient.getAvatarPath("newsessiony")).andThrow(exception);
    sessionStore.setSessionToken(null);
    replay(sessionStore);
    replay(lowLevelClient);

    try {
      client.getAvatarPath();
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertSame(exception, e);
    }
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldNotRequestSessionTokenIfNew() throws Exception {
    // We don't call login more than once per request.
    client.setCredentials("nickname", "password");

    BmsClient.LoginResult loginResult = new BmsClient.LoginResult() {
      @Override
      public String getSessionToken() {
        return "sessiony";
      }
    };
    expect(sessionStore.getSessionToken()).andReturn(null);
    expect(lowLevelClient.login("nickname", "password")).andReturn(loginResult);
    sessionStore.setSessionToken("sessiony");
    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_SESSION, "Ohno!");
    expect(lowLevelClient.getAvatarPath("sessiony")).andThrow(exception);
    sessionStore.setSessionToken(null);
    replay(sessionStore);
    replay(lowLevelClient);

    try {
      client.getAvatarPath();
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertSame(exception, e);
    }
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldRetainOldCredentialsIfLoginFailed() throws Exception {
    client.setCredentials("nickname", "password");

    BmsApiException exception = new BmsApiException(ErrorCode.INVALID_USERNAME, "Fail.");
    expect(lowLevelClient.login("newnickname", "secretpassword")).andThrow(exception);
    replay(lowLevelClient);
    replay(sessionStore);

    try {
      client.login("newnickname", "secretpassword");
      fail("Expected BmsApiException");
    } catch(BmsApiException e) {
      assertSame(e, exception);
    }
    verify(lowLevelClient);
    reset(lowLevelClient);
    reset(sessionStore);

    // Now try something that requires a login.  It should use the old
    // credentials.  Just for fun, let's pretend that they didn't work either.
    expect(sessionStore.getSessionToken()).andReturn(null);
    expect(lowLevelClient.login("nickname", "password")).andThrow(exception);
    replay(lowLevelClient);
    replay(sessionStore);

    try {
      client.getAvatarPath();
      fail("Expected BmsApiException");
    } catch(BmsApiException e) {
      assertSame(e, exception);
    }
    verify(lowLevelClient);
    verify(sessionStore);
  }

  public void testShouldAllowAddingEan() throws Exception {
    expect(sessionStore.getSessionToken()).andReturn("sessiony");
    replay(sessionStore);
    BmsClient.AddEanResult dummyResult = new BmsClient.AddEanResult() {
      @Override
      public String getErrorMessage() {
        throw new UnsupportedOperationException();
      }

      @Override
      public String getImageUrl() {
        throw new UnsupportedOperationException();
      }

      @Override
      public String getMessage() {
        throw new UnsupportedOperationException();
      }

      @Override
      public String getTitle() {
        throw new UnsupportedOperationException();
      }
    };
    expect(lowLevelClient.addEan("sessiony", "1234", BmsClient.ShareWith.PUBLIC))
        .andReturn(dummyResult);
    replay(lowLevelClient);

    assertSame(dummyResult, client.addEan("1234", BmsClient.ShareWith.PUBLIC));
    verify(lowLevelClient);
    verify(sessionStore);
  }
}
